/**
 * Copyright (C), 2020, 安徽贝慕信息科技有限公司
 * FileName: SignatureVerify
 * Author:   Allen
 * Date:     2020/8/19 15:48
 * Description: 签名工具校验类
 * History:
 * <author>          <time>          <version>          <desc>
 * 作者姓名           修改时间           版本号              描述
 */
package com.bim.marvel.gateway.core.manager;

import com.bim.marvel.common.core.model.dto.SimpleVO;
import com.bim.marvel.common.exception.ServiceException;
import com.bim.marvel.common.util.GatewayUtils;
import com.bim.marvel.gateway.client.constant.GatewayCodeEnum;
import com.bim.marvel.gateway.client.constant.GatewayCommonConstants;
import com.bim.marvel.openapi.client.dto.AppDetailDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;
import java.util.Map;

import static com.bim.marvel.gateway.client.constant.GatewayCommonConstants.*;

/**
 * 〈签名工具校验类〉
 *
 * @author Allen
 * @date 2020/8/19
 * @since 1.0.0
 */
@Slf4j
@Component
public class SignatureVerify {

    /**
     * RedisTemplate
     */
    private static RedisTemplate<Object, Object> redisTemplate;

    /**
     * 由于Service无法注入，故而通过架造器方法实现
     */
    @Autowired
    public SignatureVerify(RedisTemplate redisTemplate) {
        SignatureVerify.redisTemplate = redisTemplate;
    }

    /**
     * 校验签名
     * @param params 签名入参
     * @param appId  AppID
     * @return  返回校验通过获得到的token值
     */
    public String checkSignature(Map<String, String> params, String appId) {
        //先进行参数校验
        checkSignRequestParams(params);
        // 直接通过判断marvel-open-api项目保存在Redis的对象信息
        BoundValueOperations<Object, Object> appDetailBO = redisTemplate.boundValueOps(GET_APP_KEY_INFO
                + appId);
        AppDetailDTO appDetailDTO = null;
        if (appDetailBO != null) {
            SimpleVO<AppDetailDTO> detailDTOSimpleVO = (SimpleVO<AppDetailDTO>) appDetailBO.get();
            appDetailDTO = detailDTOSimpleVO.getData();
        }
        if (appDetailDTO == null) {
            throw new ServiceException(GatewayCodeEnum.APP_ID_ERROR);
        }
        if (!StringUtils.equals(appDetailDTO.getSessionKey(),
                params.get(GatewayCommonConstants.REQUEST_SESSION_KEY_PARAMETER_NAME))) {
            throw new ServiceException(GatewayCodeEnum.SESSION_KEY_ERROR);
        }
        if (appDetailDTO.getExpiredDate().before(new Date())) {
            throw new ServiceException(GatewayCodeEnum.EXPIREDDATE_INVALID);
        }
        if (StringUtils.isEmpty(appDetailDTO.getToken())) {
            throw new ServiceException(GatewayCodeEnum.TOKEN_NOT_FOUND);
        }
        // 客户端参数签名字符串
        String clientSign = params.remove(GatewayCommonConstants.REQUEST_SIGN_PARAMETER_NAME);
        // 服务端签名字符串，根据请求参数进行签名
        String serverSign = null;
        try {
            // 移除请求资源服务ID参数(因为客户端无须传此值，这个为后端增加，在此比较时故而须移除)
            params.remove(GatewayCommonConstants.REQUEST_SERVICE_ID);
            serverSign = new GatewayUtils().signRequest(params, appDetailDTO.getAppSecret(),
                    params.get(GatewayCommonConstants.REQUEST_SIGN_METHOD_PARAMETER_NAME));
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 比较服务端签名和客户端签名
        if (!StringUtils.equalsAnyIgnoreCase(serverSign, clientSign)) {
            throw new ServiceException(GatewayCodeEnum.INVALID_SIGN);
        }
        return appDetailDTO.getToken();
    }

    /**
     * 请求参数校验
     *
     * @param params 经过处理的请求参数
     */
    private void checkSignRequestParams(Map<String, String> params) {
        String appId = params.get(GatewayCommonConstants.REQUEST_APP_ID_PARAMETER_NAME);
        String sessionKey = params.get(GatewayCommonConstants.REQUEST_SESSION_KEY_PARAMETER_NAME);
        String sign = params.get(GatewayCommonConstants.REQUEST_SIGN_PARAMETER_NAME);
        String signMethod = params.get(GatewayCommonConstants.REQUEST_SIGN_METHOD_PARAMETER_NAME);
        String timestampValue = params.get(GatewayCommonConstants.REQUEST_TIMESTAMP_PARAMETER_NAME);
        String uri = params.get(GatewayCommonConstants.REQUEST_URI_PARAMETER_NAME);
        String serviceId = params.get(GatewayCommonConstants.REQUEST_SERVICE_ID);
        if (StringUtils.isEmpty(appId)) {
            throw new ServiceException(GatewayCodeEnum.APP_ID_EMPTY);
        }
        if (StringUtils.isEmpty(sessionKey)) {
            throw new ServiceException(GatewayCodeEnum.SESSION_KEY_EMPTY);
        }
        if (StringUtils.isEmpty(timestampValue)) {
            throw new ServiceException(GatewayCodeEnum.TIMESTAMP_EMPTY);
        }
        if (StringUtils.isEmpty(signMethod)) {
            throw new ServiceException(GatewayCodeEnum.SIGN_METHOD_EMPTY);
        } else {
            boolean md5 = GatewayUtils.MD5.equals(signMethod);
            boolean hmac = GatewayUtils.HMAC.equals(signMethod);
            boolean hmacSha256 = GatewayUtils.HMAC_SHA256.equals(signMethod);
            if (!(md5 || hmac || hmacSha256)) {
                throw new ServiceException(GatewayCodeEnum.SIGN_METHOD_ERROR);
            }
        }
        if (StringUtils.isEmpty(sign)) {
            throw new ServiceException(GatewayCodeEnum.SIGN_EMPTY);
        }
        // 直接通过判断marvel-open-api项目保存在Redis的键值是否存在
        Boolean backendApiExist = redisTemplate.hasKey(GET_OPEN_API_URI + serviceId
                + GET_OPEN_API_CONNECTOR + uri);
        if (!backendApiExist) {
            //因为存在@PathVariable请求方式，故而过滤掉最后的参数
            uri = uri.substring(0, uri.lastIndexOf("/"));
            backendApiExist = redisTemplate.hasKey(GET_OPEN_API_URI + serviceId
                    + GET_OPEN_API_CONNECTOR + uri);
            if (!backendApiExist) {
                throw new ServiceException(GatewayCodeEnum.INVALID_URI_REQUEST);
            }
        }
        try {
            long timestamp = Long.parseLong(timestampValue);
            Date clientTime = new Date(timestamp * ONE_THOUSAND_MILLISECONDS);
            Date endTime = DateUtils.addMinutes(clientTime, TIMESTAMP_VALIDITY);
            Date currentTime = new Date();
            // 用户发起请求时的unix时间戳，本次请求签名的有效时间为该时间戳+10分钟。
            if (endTime.before(currentTime)) {
                throw new ServiceException(GatewayCodeEnum.REQUEST_URL_TIMEOUT);
            }
        } catch (NumberFormatException e) {
            throw new ServiceException(GatewayCodeEnum.TIMESTAMP_ERROR);
        }
    }
}
